【AWS CDK】AWS Fargate で Windows コンテナを起動してみた

【AWS CDK】AWS Fargate で Windows コンテナを起動してみた

はじめに

テントの中から失礼します。製造ビジネステクノロジー部の「てんとタカハシ」です!

Fargate では Linux コンテナが多く利用される一方、Windows コンテナを利用することも可能です。例えば、Windows Server 上で稼働している ASP.NET などのアプリケーションをコンテナ化して、Fargate 上で起動するといったニーズにも対応できます。

本記事では Fargate 上で Windows コンテナを起動する構成を CDK で実装します。この内容は AWS 公式ブログの記事「Running Windows Containers with Amazon ECS on AWS Fargate」を参考にしています。

本記事で実装するソースコードは GitHub リポジトリでも公開していますので、併せてご参照ください。

iam326/fargate-windows-container

AWS 構成

本記事で実装する構成は下記の通りです。検証のためシンプルで低コストな構成にしています。

fargate-windows-sample-devio.drawio

  • ALB を Public Subnet、Windows コンテナを Private Subnet に配置する
  • Docker イメージは NAT Gateway を経由して DockerHub から取得する
  • Windows Server 2019 ベースの IIS イメージ「mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019」を使用する
    • 2 GB 程度の比較的大きいイメージなので、起動に時間を要する点にご留意ください

成果物

ブラウザで http://ALB のデフォルト DNS 名(例: http://my-alb-1234567890.ap-northeast-1.elb.amazonaws.com)にアクセスすると IIS のデフォルトページが表示されます。タスクの設定も期待通りです。

aws-fargate-windows-container-demo

料金

Windows コンテナの料金 には OS のライセンス料が上乗せされます。リソースについても vCPU は「1」、メモリは「2」GB が最低限必要になることから、Linux と比べて料金が高くなりやすいです。

一例として、東京リージョンでの料金を記載します(2025/01/13 時点)。

  • Linux/X86
    • 1 時間あたりの vCPU 単位: 0.05056 USD
    • 1 時間あたりの GB 単位: 0.00553 USD
  • Windows/X86
    • 1 時間あたりの vCPU 単位: 0.058144 USD
    • 1 時間あたりの GB 単位: 0.0063595 USD
    • OS ライセンス料 - 1 時間あたりの vCPU 単位: 0.046 USD

開発環境

開発環境は下記の通りです。

% sw_vers
ProductName:		macOS
ProductVersion:		14.6.1
BuildVersion:		23G93

% aws --version
aws-cli/2.22.7 Python/3.12.6 Darwin/23.6.0 exe/x86_64

% cdk --version
2.167.2 (build 3669dce)

実装のポイント

Fargate を CDK で実装する内容については、既に多くの記事で紹介されていますので、今回のポイントになる部分を先に解説します。

OS の指定

タスク定義の runtimePlatform で Windows を指定します。Linux コンテナの定義と比較する場合、この設定が主な差分になります。

fargate.ts
// ECS Task Definition
const taskDefinition = new ecs.FargateTaskDefinition(
  this,
  'ECSTaskDefinition',
  {
    family: `${projectName}-ecs-task-definition`,
    cpu,
    memoryLimitMiB: memory,
    executionRole,
    taskRole,
+   // Windows Server 2019 を指定する
+   runtimePlatform: {
+     cpuArchitecture: ecs.CpuArchitecture.X86_64,
+     operatingSystemFamily: ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE,
+   },
  }
);

Docker イメージの取得

DockerHub から Docker イメージを取得します。実際の開発では ECR から取得することが一般的になると思います。

fargate.ts
// ECS Task Container
const container = taskDefinition.addContainer('ECSTaskDefinitionContainer', {
+ // DockerHub からイメージを取得する
+ image: ecs.ContainerImage.fromRegistry(
+   'mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019'
+ ),
  containerName: `${projectName}-ecs-task-container`,
  cpu,
  memoryLimitMiB: memory,
  memoryReservationMiB: memory,
  logging: ecs.LogDriver.awsLogs({
    streamPrefix: projectName,
    logGroup,
  }),
});

ソースコード

各スタックの実装になります。iam326/fargate-windows-container では CDK プロジェクト全体を公開しています。

VPC

vpc.ts
import * as cdk from 'aws-cdk-lib';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import { Construct } from 'constructs';

type VpcStackProps = cdk.StackProps & {
  projectName: string;
};

export class VpcStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: VpcStackProps) {
    super(scope, id, props);

    const { projectName } = props;

    const cidr = '10.100.0.0/16';
    const cidrMask = 24;
    const maxAzs = 2;
    const natGateways = 1;

    // VPC
    new ec2.Vpc(this, 'Vpc', {
      vpcName: `${projectName}-vpc`,
      ipAddresses: ec2.IpAddresses.cidr(cidr),
      subnetConfiguration: [
        {
          cidrMask,
          name: 'public',
          subnetType: ec2.SubnetType.PUBLIC,
          mapPublicIpOnLaunch: false,
        },
        {
          cidrMask,
          name: 'private',
          subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
        },
      ],
      maxAzs,
      natGateways,
    });
  }
}

Fargate & ALB

fargate.ts
import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as ecs from 'aws-cdk-lib/aws-ecs';
import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as logs from 'aws-cdk-lib/aws-logs';

type FargateStackProps = cdk.StackProps & {
  projectName: string;
};

export class FargateStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: FargateStackProps) {
    super(scope, id, props);

    const { projectName } = props;

    const port = 80;
    const cpu = 1024;
    const memory = 2048;
    const desiredCount = 1;

    // VPC
    const vpc = ec2.Vpc.fromLookup(this, 'Vpc', {
      vpcName: `${projectName}-vpc`,
    });

    // Security Group
    const albSecurityGroup = new ec2.SecurityGroup(this, 'ALBSecurityGroup', {
      vpc,
      securityGroupName: `${projectName}-alb-security-group`,
      allowAllOutbound: true,
    });
    albSecurityGroup.addIngressRule(
      ec2.Peer.anyIpv4(),
      ec2.Port.tcp(port),
      'Allow HTTP traffic'
    );

    const serviceSecurityGroup = new ec2.SecurityGroup(
      this,
      'ECSServiceSecurityGroup',
      {
        vpc,
        securityGroupName: `${projectName}-ecs-service-security-group`,
        allowAllOutbound: true,
      }
    );
    serviceSecurityGroup.addIngressRule(
      albSecurityGroup,
      ec2.Port.tcp(port),
      'Allow traffic from ALB'
    );

    // CloudWatch Logs
    const logGroup = new logs.LogGroup(this, 'ECSServiceLogGroup', {
      logGroupName: `${projectName}-cloudwatch-logs`,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // IAM Role
    const executionRole = new iam.Role(this, 'EcsTaskExecutionRole', {
      roleName: `${projectName}-ecs-task-execution-role`,
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
      managedPolicies: [
        iam.ManagedPolicy.fromAwsManagedPolicyName(
          'service-role/AmazonECSTaskExecutionRolePolicy'
        ),
      ],
    });

    const taskRole = new iam.Role(this, 'EcsTaskRole', {
      roleName: `${projectName}-ecs-task-role`,
      assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'),
    });

    // ECS Cluster
    const cluster = new ecs.Cluster(this, 'EcsCluster', {
      vpc,
      clusterName: `${projectName}-ecs-cluster`,
      containerInsights: true,
    });

    // ECS Task Definition
    const taskDefinition = new ecs.FargateTaskDefinition(
      this,
      'ECSTaskDefinition',
      {
        family: `${projectName}-ecs-task-definition`,
        cpu,
        memoryLimitMiB: memory,
        executionRole,
        taskRole,
        // Windows Server 2019 を指定する
        runtimePlatform: {
          cpuArchitecture: ecs.CpuArchitecture.X86_64,
          operatingSystemFamily:
            ecs.OperatingSystemFamily.WINDOWS_SERVER_2019_CORE,
        },
      }
    );

    // ECS Task Container
    const container = taskDefinition.addContainer(
      'ECSTaskDefinitionContainer',
      {
        // DockerHub から Docker イメージを取得する
        image: ecs.ContainerImage.fromRegistry(
          'mcr.microsoft.com/windows/servercore/iis:windowsservercore-ltsc2019'
        ),
        containerName: `${projectName}-ecs-task-container`,
        cpu,
        memoryLimitMiB: memory,
        memoryReservationMiB: memory,
        logging: ecs.LogDriver.awsLogs({
          streamPrefix: projectName,
          logGroup,
        }),
      }
    );
    container.addPortMappings({
      containerPort: port,
      hostPort: port,
      protocol: ecs.Protocol.TCP,
    });

    // ECS Fargate Service
    const fargateService = new ecs.FargateService(this, 'EcsFargateService', {
      serviceName: `${projectName}-ecs-fargate-service`,
      cluster,
      vpcSubnets: vpc.selectSubnets({ subnetGroupName: 'private' }),
      securityGroups: [serviceSecurityGroup],
      taskDefinition: taskDefinition,
      desiredCount,
      maxHealthyPercent: 200,
      minHealthyPercent: 100,
    });

    // ALB
    const alb = new elbv2.ApplicationLoadBalancer(this, 'ALB', {
      vpc,
      loadBalancerName: `${projectName}-alb`,
      internetFacing: true,
      crossZoneEnabled: true,
      securityGroup: albSecurityGroup,
    });

    // ALB Listener
    const listener = alb.addListener('ALBListener', {
      port,
    });
    listener.addTargets('ALBListenerTarget', {
      port,
      targets: [fargateService],
    });
  }
}

おわりに

本記事では Fargate 上で Windows コンテナを起動する構成を CDK で実装してみました。Linux コンテナと比較して、実装の差分が少ないため、簡単に起動することができました。

Windows アプリケーションのモダナイズにおいて、コンテナ化や Fargate は欠かせない要素の一つです。今後も Fargate で Windows コンテナを動かす実践的なノウハウを情報発信していきたいと思います。

最後までお読みいただきありがとうございました!この記事が少しでも参考になれば幸いです。

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.